import os
import cPickle

from utils.persist import Persistent
from utils.namespace import Namespace

from cranesched.routing import Router
from cranesched.instance.warehouse.crane import Crane
from cranesched.instance.activity import Activity

from cranesched.solution.assignment import Assignment
from cranesched.solution.assignmentlist import AssignmentList
from cranesched.solution.activitymgr import ActivityManager
from cranesched.solution.domainmgr import DomainManager


class CompleteSolution(Exception):
    """Exception raised to indicate that the solution is complete, i.e. all activities have been 
    assigned to cranes and the solution is feasible."""
    pass
    
class InfeasibleSolution(Exception):
    """Exception raised to indicate that the solution is infeasible, i.e. at least one hard 
    constraint is violated."""
    pass
    
    
class Solution(Persistent):
    Complete   = CompleteSolution
    Infeasible = InfeasibleSolution
    
    def __init__(self, instance, **meta):
        prec_graph = instance.prec_graph
        crane_ids = set(c.id for c in instance.warehouse.cranes)
        self.instance = instance                             # cranesched.Instance
        self.meta = Namespace(meta)                          # solution metadata
        self.activities = ActivityManager(prec_graph)        # tracks assignable activities
        self.domains = DomainManager(prec_graph, crane_ids)  # manages activities' domains
        self.assignments = AssignmentList()                  # records the assignment sequence
        self.warehouse = instance.warehouse.deepcopy()       # cranesched.Warehouse
        self.router = Router(lo=self.warehouse.xlimits[0],   # used to plan crane routes
                             hi=self.warehouse.xlimits[1],
                             time=min(c.time for c in self.warehouse.cranes))
        # link cranes to router and compute initial movement bounds for all cranes
        for crane in self.warehouse.cranes:
            crane.set_router(self.router)
        self.router.update()
        # apply cranes' initial stocks to domains
        self.domains.set_initial_stocks({c.id: c.items for c in self.warehouse.cranes})
        
    def __str__(self):
        lines = [object.__repr__(self), "[[ Assignments ]]"]
        for assignment in self.assignments:
            lines.append("\t%s" % assignment)
        lines.append("[[ Cranes ]]")
        for _, crane in sorted(self.warehouse.cranes, key=lambda c: c.id):
            lines.append("\t%s" % crane)
        return "\n".join(lines)
        
    @property
    def is_complete(self):
        return len(self.assignments) == len(self.instance.prec_graph)
        
    @property
    def makespan(self):
        return max(crane.time for crane in self.warehouse.cranes)
        
    @property
    def total_traveled_distance(self):
        return sum(crane.traveled_distance for crane in self.warehouse.cranes)
        
    @property
    def total_penalty(self):
        return sum(assignment.penalty for assignment in self.assignments)
        
    # --------------------------------------------------------------------------
    # Methods for adding assignments to the solution, and undoing the last assignment
    def append(self, assignment):
        assert assignment.activity in self.activities.assignable
        self.activities.mark_assigned(assignment.activity)
        self.domains.update(assignment.activity, assignment.crane.id)
        self.assignments.append(assignment)
        assignment.crane.fulfill(assignment)
        return assignment
        
    def pop(self):
        assignment = self.assignments.pop()
        self.activities.mark_unassigned(assignment.activity)
        self.domains.restore()
        assignment.crane.undo()
        return assignment
        
    def assign(self, activity, crane):
        if not isinstance(activity, Activity):
            activity = self.instance.prec_graph[activity]
        if not isinstance(crane, Crane):
            crane = self.warehouse[crane]
        return self.append(Assignment(self, activity, crane))
        
    def eligible_assignments(self, activity=None):
        """An iterator over the assignments which are currently possible for a given activity. If 
        no activity is specified, assignments are computed for all assignable activities."""
        if self.is_complete:
            raise Solution.Complete("all activities have been assigned")
        assignments = []
        if activity is None:
            # generate assignments for *all* assignable activities
            for activity in self.activities.assignable:
                assignments.extend(self.eligible_assignments(activity))
            if len(assignments) == 0:
                raise Solution.Infeasible("unable to find eligible assignments")
        else:
            # generate assignments for a particular activity
            for crane_id in self.domains[activity]:
                try:
                    assignment = Assignment(self, activity, self.warehouse[crane_id])
                except Assignment.Error: #as error:
                    # print "A%s -> C%s :: %s - %s" % (activity.id, crane_id, 
                    #                                  type(error).__name__, error.message)
                    # raise  # comment this line out later
                    pass
                else:
                    assignments.append(assignment)
        return assignments
        
    # --------------------------------------------------------------------------
    # Persistence protocol
    pickle_module = cPickle
    pickle_protocol = 2
    
    def __presave__(self, filepath, **kwargs):
        if filepath is None and "filepath" not in self.meta:
            raise Exception("output file path required")
        elif filepath is not None:
            filepath = os.path.abspath(filepath)
            directory, filename = os.path.split(filepath)
            self.meta.filepath = filepath
            self.meta.filename = filename
            self.meta.directory = directory
        self.instance.prec_graph.unlink()
        kwargs.setdefault("pickle", type(self).pickle_module)
        kwargs.setdefault("protocol", type(self).pickle_protocol)
        return (self.meta.filepath,), kwargs
        
    def __postsave__(self, *args, **kwargs):
        self.instance.prec_graph.link()
        
    @classmethod
    def __preload__(cls, filepath, **kwargs):
        kwargs.setdefault("pickle", cls.pickle_module)
        kwargs.setdefault("protocol", cls.pickle_protocol)
        return (filepath,), kwargs
        
    def __postload__(self, *args, **kwargs):
        self.instance.prec_graph.link()
        
        
